﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace NMemcached.Client
{
    /// <summary>
    /// Hashes an array of bytes with the FNV1 or FNV1a algorithm
    /// </summary>
    public sealed class FNVManaged : HashAlgorithm
    {
        private const uint FNV32_PRIME = 16777619;
        private const uint FNV32_OFFSET = 2166136261;

        private const ulong FNV64_PRIME = 0x100000001b3L;
        private const ulong FNV64_OFFSET = 0xcbf29ce484222325L;

        /// <summary>
        /// Defines the FNV hashing algorithms available.
        /// </summary>
        public enum FNVAlgorithm
        {
            /// <summary>
            /// The FNV1 hash algorithm
            /// </summary>
            FNV1,
            /// <summary>
            /// The FNV1a hash algorithm
            /// </summary>
            FNV1a
        };

        private uint _current32;
        private ulong _current64;

        private bool _is64Bit;

        /// <summary>
        /// Gets the algorithm being used by the current instance.
        /// </summary>
        public FNVAlgorithm Algorithm { get; private set; }

        /// <summary>
        /// Initializes a new instance of the FNVManaged class
        /// </summary>
        public FNVManaged()
            : this(FNVAlgorithm.FNV1a, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the FNVManaged class
        /// </summary>
        /// <param name="algorithm">The FNV algorithm used to generate the hash value</param>
        public FNVManaged(FNVAlgorithm algorithm)
            : this(algorithm, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the FNVManaged class
        /// </summary>
        /// <param name="create64bit">Determines if a 64 bit hash is generated. A 32 bit hash is generated by default.</param>
        public FNVManaged(bool create64bit)
            : this(FNVAlgorithm.FNV1a, create64bit)
        {
        }

        /// <summary>
        /// Initializes a new instance of the FNVManaged class
        /// </summary>
        /// <param name="algorithm">The FNV algorithm used to generate the hash value</param>
        /// <param name="create64bit">Determines if a 64 bit hash is generated. A 32 bit hash is generated by default.</param>
        public FNVManaged(FNVAlgorithm algorithm, bool create64bit)
            : base()
        {
            this.Algorithm = algorithm;
            this._is64Bit = create64bit;

            if (create64bit)
            {
                this.HashSizeValue = 64;
            }
            else
            {
                this.HashSizeValue = 32;
            }
        }

        /// <summary>
        /// Initializes the FNVManaged instance
        /// </summary>
        public override void Initialize()
        {
        }

        /// <summary>
        /// Returns a string representation of the FNVManaged class
        /// </summary>
        /// <returns>A string representation of the FNVManaged class</returns>
        public override string ToString()
        {
            return String.Format("NMemcached.Client.FNVManaged-{0}-{1}", this.Algorithm, this.HashSize);
        }

        /// <summary>
        /// Routes data written to the object into the hash algorithm for computing the hash.
        /// </summary>
        /// <param name="array">The input data. </param>
        /// <param name="ibStart">The offset into the byte array from which to begin using data. </param>
        /// <param name="cbSize">The number of bytes in the array to use as data. </param>
        protected override void HashCore(byte[] array, int ibStart, int cbSize)
        {
            this._current32 = FNV32_OFFSET;
            this._current64 = FNV64_OFFSET;

            switch (this.Algorithm)
            {
                case FNVAlgorithm.FNV1:
                    this.HashCoreFNV(array, ibStart, cbSize);
                    break;
                case FNVAlgorithm.FNV1a:
                default:
                    this.HashCoreFNV1a(array, ibStart, cbSize);
                    break;
            }
        }

        /// <summary>
        /// Returns the computed hash value after all data has been written to the object.
        /// </summary>
        /// <returns>The computed hash code.</returns>
        protected override byte[] HashFinal()
        {
            if (this._is64Bit)
            {
                return BitConverter.GetBytes(this._current64);
            }
            else
            {
                return BitConverter.GetBytes(this._current32);
            }
        }

        //Private methods
        private void HashCoreFNV(byte[] array, int ibStart, int cbSize)
        {
            int end = ibStart + cbSize;

            for (int i = ibStart; i < end; i++)
            {
                if (this._is64Bit)
                {
                    this._current64 = (this._current64 * FNV64_PRIME) ^ array[i];
                }
                else
                {
                    this._current32 = (this._current32 * FNV32_PRIME) ^ array[i];
                }
            }
        }

        private void HashCoreFNV1a(byte[] array, int ibStart, int cbSize)
        {
            int end = ibStart + cbSize;

            for (int i = ibStart; i < end; i++)
            {
                if (this._is64Bit)
                {
                    this._current64 = (this._current64 ^ array[i]) * FNV64_PRIME;
                }
                else
                {
                    this._current32 = (this._current32 ^ array[i]) * FNV32_PRIME;
                }
            }
        }
    }
}
